package edu.uwm.cs552;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.List;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Observable;
import java.util.Set;

import javax.swing.ImageIcon;

import edu.uwm.cs552.gfx.SVGImageCreator;
import edu.uwm.cs552.util.FormatException;

/**
 * The fixed part of a hexagonal game board.
 * We have a back image, terrain and barriers (rivers) for certain coordinates.
 */
public class HexBoard extends Observable {
	/**
	 * We may need to serialize this class.
	 */
	private static final long serialVersionUID = 1L;
	
	private SVGImageCreator background;
	private double naturalWidth, naturalHeight;
	private Map<HexCoordinate,Terrain> terrains = new HashMap<HexCoordinate,Terrain>();
	private Map<HexEdge,Barrier> barriers = new HashMap<HexEdge,Barrier>();
	private Map<HexCoordinate,City> cities = new HashMap<HexCoordinate,City>();
	private Map<HexCoordinate,ArrayList<Good>> sources = new HashMap<HexCoordinate,ArrayList<Good>>();
	
	public static final int DEFAULT_WIDTH = 75;
	public static final int DEFAULT_HEIGHT = 50;
	
	public HexBoard() {
		initialize();
	}
	
	private void initialize() {
		background = null;
		naturalWidth = DEFAULT_WIDTH;
		naturalHeight = DEFAULT_HEIGHT;		
	}
	
	/**
	 * Create a hex board with the given background image
	 * and natural width and height (in terms of hexagon widths).
	 * @param im background image, may be null
	 * @param w width
	 * @param h height
	 */
	public HexBoard(SVGImageCreator im, double w, double h) {
		background = im;
		naturalWidth = w;
		naturalHeight = h;
	}
	
	public Collection<HexCoordinate> allCityLocations() {
		return cities.keySet();
	}
	
	public City getCity(HexCoordinate h) {
		return cities.get(h);
	}
	
	public City putCity(HexCoordinate h, City c) {
		City result = cities.put(h, c);
		super.setChanged();
		super.notifyObservers(h);
		return result;
	}
	
	public ArrayList<Good> getGoods(HexCoordinate h) {
		return sources.get(h);
	}
	
	public Collection<HexCoordinate> getSources(Good g) {
		return sources.keySet();
	}
	
	public Terrain getTerrain(HexCoordinate h) {
		return terrains.get(h);
	}
	
	public Terrain putTerrain(HexCoordinate h, Terrain t) {
		Terrain result = terrains.put(h, t);
		super.setChanged();
		super.notifyObservers(h);
		return result;
	}
	
	public Terrain removeTerrain(HexCoordinate h) {
		final Terrain result = terrains.remove(h);
		super.setChanged();
		super.notifyObservers(h);		
		return result;
	}
	
	public Barrier getBarrier(HexEdge e) {
		return barriers.get(e);
	}
	
	public Barrier putBarrier(HexEdge e, Barrier b) {
		final Barrier result = barriers.put(e, b);
		super.setChanged();
		super.notifyObservers(e);				
		return result;
	}
	
	public Barrier removeBarrier(HexEdge e) {
		final Barrier result = barriers.remove(e);
		super.setChanged();
		super.notifyObservers(e);	
		return result;
	}
	
	public void setBackground(SVGImageCreator im, double w, double h) {
		background = im;
		naturalWidth = w;
		naturalHeight = h;
		super.setChanged();
		super.notifyObservers(this);
	}
	
	public void clear() {
		initialize();
		terrains.clear();
		barriers.clear();		
		super.setChanged();
		super.notifyObservers(this);
	}
	
	/**
	 * Copy everything from the other board to this.
	 * @param b board to copy, must not be null
	 */
	public void copy(HexBoard b) {
		terrains.clear();
		barriers.clear();
		terrains.putAll(b.terrains);
		barriers.putAll(b.barriers);
		setBackground(b.background,b.naturalWidth,b.naturalHeight);
	}
	
	private static final double SQRT32 = Math.sqrt(3)/2; // ##
	
	/**
	 * Draw the game board into the given graphics context.
	 * @param g graphics context, must not be null
	 * @param scale width of hexagons
	 * @param showHidden whether to draw implicit barriers and suburbs
	 * @param drawMesh whether to draw the outline of all hexagons
	 */
	public void draw(Graphics g, double scale, boolean showHidden, boolean drawMesh) {
		if (background != null) {
			background.draw(g, getWidth(scale), getHeight(scale));
		}
		for (Map.Entry<HexCoordinate,Terrain> e : terrains.entrySet()) {
			e.getValue().draw(g, e.getKey(), scale, showHidden);
		}
		for (Map.Entry<HexEdge,Barrier> e : barriers.entrySet()) {
			e.getValue().draw(g, e.getKey(), scale, showHidden);
		}
		for (Map.Entry<HexCoordinate, City> e : cities.entrySet()) {
			if (e.getValue() != null) e.getValue().draw(g, e.getKey(), scale);
		}
		if (drawMesh) {
			g.setColor(Color.black);
			int abound = (int)Math.ceil(naturalWidth);
			int bbound = (int)Math.ceil(naturalHeight/SQRT32);
			for (int b = 0; b <= bbound; ++b) {
				for (int a = 0; a <= abound; ++a) {
					HexCoordinate h = new HexCoordinate(a+b/2,b);
					g.drawPolygon(h.toPolygon(scale));
				}
			}
		}
	}

	/**
	 * Get the height of the board (the height of the background image)
	 * given the current scale.
	 * @param scale scale to use
	 * @return height of board.
	 */
	public int getHeight(double scale) {
		return (int)(naturalHeight*scale);
	}

	/**
	 * Get the width of the board (the width of the background image)
	 * given the current scale.
	 * @param scale scale to use
	 * @return width of board
	 */
	public int getWidth(double scale) {
		return (int)(naturalWidth*scale);
	}

	/**
	 * Write a hexboard out using the form:
	 * <pre>
	 * <i>URL (or empty, if no background image)</i>
	 * <i>width</i>
	 * <i>height</i>
	 * <i>[The following section repeated indefinitely]</i>
	 * <i>terrain</i>
	 * <i>hexcoordinate<sb>1</db></i>
	 * <i>hexcoordinate<sb>2</db></i>
	 * ...
	 * <i>hexcoordinate<sb>n</db></i>
	 * OR
	 * <i>barrier</i>
	 * <i>hexedge<sb>1</db></i>
	 * <i>hexedge<sb>2</db></i>
	 * ...
	 * <i>hexedge<sb>n</db></i>
	 * </pre>
	 * @param pw output to use, must not be null
	 */
	public void write(PrintWriter pw) {
		if (background == null) {
			pw.println();
		} else {
			pw.println(background.getURL().toExternalForm());
		}
		pw.println(naturalWidth);
		pw.println(naturalHeight);
		Good.writeGoods(pw);
		//TODO: Write Cities
		Terrain previous = null;
		for (Map.Entry<HexCoordinate,Terrain> e : terrains.entrySet()) {
			if (e.getValue() == null) continue;
			if (e.getValue() != previous) {
				previous = e.getValue();
				pw.println(e.getValue());
			} 
			pw.print(e.getKey());
			if (e.getValue() == Terrain.SMALL || e.getValue() == Terrain.MEDIUM || e.getValue() == Terrain.LARGE)
				pw.print(cities.get(e.getKey()).toWrite());
			pw.print("\n");
		}
		Barrier barrier = null;
		for (Map.Entry<HexEdge,Barrier> e : barriers.entrySet()) {
			if (e.getValue() == null) continue;
			if (e.getValue() != barrier) {
				barrier = e.getValue();
				pw.println(e.getValue());
			}
			pw.println(e.getKey());
		}
	}
	
	private static final Set<String> ALL_TERRAINS = new HashSet<String>();
	private static final Set<String> ALL_BARRIERS = new HashSet<String>();
	static {
		for (Terrain t : Terrain.values()) {
			ALL_TERRAINS.add(t.name());
		}
		for (Barrier b : Barrier.values()) {
			ALL_BARRIERS.add(b.name());
		}
	}
	
	/**
	 * Read in a hex board using the format given in {@link #write(PrintWriter)}.
	 * Throw an appropriate exception given a problem.
	 * @param br buffered reader to use, must not be null
	 * @return a new hex board
	 * @throws IOException if there is an error reading the file, or if the URL is bad, or the SVG in it
	 * @throws FormatException an error reading a hex coordinate or terrain
	 * @throws NumberFormatException an error reading the width or height
	 */
	public void read(BufferedReader br) throws IOException, FormatException, NumberFormatException {
		String url = br.readLine();
		double newWidth = Double.parseDouble(br.readLine());
		double newHeight = Double.parseDouble(br.readLine());
		Terrain terrain = null;
		Barrier barrier = null;
		String s; 
		SVGImageCreator im = url.equals("") ? null : new SVGImageCreator(new URL(url));
		Map<HexCoordinate,Terrain> newTerrains = new HashMap<HexCoordinate,Terrain>();
		Map<HexEdge,Barrier> newBarriers = new HashMap<HexEdge,Barrier>();
		Map<HexCoordinate,City> newCities = new HashMap<HexCoordinate,City>();
		Map<HexCoordinate,ArrayList<Good>> newSources = new HashMap<HexCoordinate,ArrayList<Good>>();

		s = Good.readGoods(br);
		do {
			if (ALL_TERRAINS.contains(s)) {
				barrier = null;
				terrain = Terrain.valueOf(s);
			} else if (ALL_BARRIERS.contains(s)) {
				terrain = null;
				barrier = Barrier.valueOf(s);
			} else if (terrain != null) {
				String hex = s, city = null;
				if ((s.indexOf('>') + 1) < s.length()) {
					hex = s.substring(0, s.indexOf('>') + 1);
					city = s.substring(s.indexOf('>') + 1);
				}
				HexCoordinate h = HexCoordinate.fromString(hex);
				if (city != null) {
					String[] stuff = city.split(",");
					City c = new City(stuff[0]);
					newCities.put(h, c);
					ArrayList<Good> goods = new ArrayList<Good>();
					for (int i = 1; i < stuff.length; i++) {
						Good g = Good.findGood(stuff[i]);
						c.addGood(g);
						goods.add(g);
					}
					newSources.put(h, goods);
				}
				newTerrains.put(h, terrain);
			} else if (barrier != null) {
				HexEdge e = HexEdge.fromString(s);
				newBarriers.put(e, barrier);
			} else {
				throw new FormatException("expected terrain or barrier");
			}
		} while ((s = br.readLine()) != null);

		terrains.clear();
		barriers.clear();
		cities.clear();
		sources.clear();
		terrains.putAll(newTerrains);
		barriers.putAll(newBarriers);
		cities.putAll(newCities);
		sources.putAll(newSources);
		setBackground(im,newWidth,newHeight);
	}
}
